Serverless GraphQL React App using AWS Amplify — Part Two

James Hamann
13 min readDec 12, 2018

Following on from my previous post, Serverless GraphQL React App using AWS Amplify — Part One, we’ll be looking at further developing the UI as well as implementing the basic CRUD (create, read, update, delete) functions.

All source code is available here.

Currently our site looks a little something like this, pretty useless at the moment…

Create

Let’s start by developing the ability to Create items. First we’ll need to add the API feature in our Amplify project, this will setup all the necessary backend resources we require using CloudFormation templates. After running amplify add api, the CLI will guide us through the setup, as below.

#bash
→ amplify add api
? Please select from one of the below mentioned services GraphQL
? Provide API name: serverlessgraphqlapp
? Choose an authorization type for the API API key
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? true
? What best describes your project:
? What best describes your project: Single object with fields (e.g., “Todo” with ID, name, des
cription)

? Do you want to edit the schema now? (Y/n) Y
? Do you want to edit the schema now? Yes
Please edit the file in your editor: your/file/path
? Press enter to continue

As we’re at the beginning stages our app, we’ll keep the auth tied to an API key, as we develop we can look at evolving this to use Cognito, Amazon’s auth service.

If you’re experienced with GraphQL, feel free to go ahead and make your own schema, otherwise you can choose to have Amplify’s CLI whip up a template schema for you. If you choose to follow the guided creation, be sure to choose to edit the schema after creation, as we’ll want to change the type name, as well as some of the item’s attributes. Below is the final GraphQL schema that describes our Item.

#amplify/backend/api/serverlessgraphqlapp/schema.jsontype Item @model {
id: ID!
name: String!
price: Int
description: String
}

A quick mention on Amplify’s GraphQL transformers, which can be used when describing a type. For our Item, we want to treat it as a @ model so our Item’s get saved in a DynamoDB table.

Different Directives offered from Amplify

There’s a few different methods here, but to keep things within scope of this post, we’ll only be using the @ model directive for now.

Once you’ve edited your schema, head over to the terminal, confirm your changes and push them up using amplify push. This command checks your local backend against what’s running in the cloud, if there’s an update or change or new feature, it provisions the necessary resources and sets everything up for you.

Another awesome feature Amplify offers is generating all of your GraphQL queries for you, based off of your schema. Obviously if you require more complex queries you can still build these manually, but as a start it gives a great foundation to build upon. Follow the CLI prompts through and ensure to choose javascript as the target language and leave the file pattern as the suggested default, this will save your queries in src/graphql/.

GraphQL schema compiled successfully. Edit your schema at your/file/path
Successfully added resource serverlessgraphqlapp locally
Some next steps:
“amplify push” will build all your local backend resources and provision it in the cloud
“amplify publish” will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud
amplify push
| Category | Resource name | Operation | Provider plugin |
| -------- | -------------------- | --------- | ----------------- |
| Api | serverlessgraphqlapp | Create | awscloudformation |
? Are you sure you want to continue? true
GraphQL schema compiled successfully. Edit your schema at your/file/path
? Do you want to generate code for your newly created GraphQL API (Y/n) Y
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions (src/graphql/**/*.js)
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions (Y/n) Y
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
⠦ Updating resources in the cloud. This may take a few minutes...
[...]✔ Code generated successfully and saved in file
✔ Generated GraphQL operations successfully and saved at src/graphql
✔ All resources are updated in the cloud
GraphQL endpoint: https://xxxxxxxxxx.appsync-api.eu-west-1.amazonaws.com/graphql
GraphQL API KEY: xxx-xxxxxxxxxxxxxxxxx

After a few minutes, everything is setup! Amplify sets up with an AWS AppSync project as well as the DynamoDB table for our Items. You’ll notice you’ll also be given your API endpoint and API key, don’t worry though, these are saved in our aws-exports.js file. At this point, I highly recommend briefly diving in to the AppSync Dashboard to view your API and get a real understanding of how AppSync works within your project.

Before moving forward the most important thing to do, to make sure everything works, is to import our aws-exports file into our App’s entry point, App.js.

#src/App.jsimport React, { Component } from 'react';
import './App.css';
import Home from './screens/home'
import Amplify from 'aws-amplify';
import aws_config from "./aws-exports";
Amplify.configure(aws_config);
class App extends Component {
render() {
return (
<Home />
);
}
}
export default App;

I’ve encountered errors countless times when forgetting this step, so make sure you add it in now. Let’s commit our changes and push up to our repo. If there’s one important thing to always remember, it’s to commit your code often and with meaningful messages. I won’t be prompting when you should commit, but for me I tend to do so once each new feature is implemented.

Now that’s done let’s hook up our front and back end so that we can start saving items in our new DynamoDB table.

First of all, let’s slightly edit our addItem form, to reflect what’s on our GraphQL schema and also check our forms are working by logging their input to the console within the handleChange function.

#src/components/addItem.jsimport React, { Component } from 'react';
import TextField from '@material-ui/core/TextField';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import AddIcon from '@material-ui/icons/Add';
import Button from '@material-ui/core/Button';
class AddItem extends Component {state = {
open: false,
itemName: '',
itemPrice: '',
itemDescription: ''
};
handleClickOpen = () => {
this.setState({ open: true });
};
handleClose = () => {
this.setState({ open: false });
};
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
console.log("Name: " + this.state.itemName + " Price: £" + this.state.itemPrice + " Description:" + this.state.itemDescription)
};
render() {
return (
<div style={{display: 'flex', flexWrap: 'wrap'}}>
<Button variant="fab" mini color="inherit" aria-label="Add" onClick={this.handleClickOpen}>
<AddIcon />
</Button>
<Dialog
open={this.state.open}
onClose={this.handleClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">Add a New Item</DialogTitle>
<DialogContent>
<TextField
style={{marginRight: 10}}
id="itemName"
label="Name"
type="string"
onChange={this.handleChange('itemName')}
/>
<TextField
style={{marginRight: 10}}
id="itemPrice"
label="Price"
type="number"
onChange={this.handleChange('itemPrice')}
/>
<TextField
style={{marginTop: 10}}
multiline
id="itemDescription"
label="Description"
type="string"
rows="4"
fullWidth
onChange={this.handleChange('itemDescription')}
/>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClose} color="primary">
Cancel
</Button>
<Button onClick={this.handleSubmit} color="primary">
Add Item
</Button>
</DialogActions>
</Dialog>
</div>
);
}
}
export default AddItem;

Open up the dev tools (cmd-option-I if you’re on a Mac) and, if everything’s working correctly, you should see the forms contents in the console.

As a side note, you might also see a warning related to deprecated Material UI Components, just ignore it. Currently I’ve been unable to identify which component is being deprecated as I’ve used examples directly from their docs and have tried to remove/add other components to rectify this to no avail. I will update this once I can track the specific issue down, however, for now it’s nothing to worry about.

Seeing as our form is working, let’s hook it up to our backend so when we hit Add Item an item is created in our DynamoDB table. Remember, GraphQL is different to a REST API in that whenever we create or update anything, it’s called a mutation. Head over here if you need a quick refresher for GraphQL. If you peak into your src/graphql/mutations.js file, you’ll see the mutations that Amplify generate for us, createItem (which we’ll be using now), updateItem and deleteItem (which we’ll come to later). The mutation is expecting 3 arguments, provided by our form, name, price and description. When submitted, this is interpreted by AppSync and the relevant action is taken.

#src/graphql/mutations.jsexport const createItem = `mutation CreateItem($input: CreateItemInput!) {
createItem(input: $input) {
id
name
price
description
}
}
`;

To make things simpler we can import all our mutations as an object and access them from there. Next we’ll create a new handleSubmit function which will pass the data from our form to our GraphQL API. Amplify’s React Components API and graphqlOperation take care of the actual request to the API, meaning it takes just a few lines of code to hook our front and back-ends together!

#src/components/addItem.js[...]
import { API, graphqlOperation } from "aws-amplify";
import * as mutations from '../graphql/mutations';
class AddItem extends Component {[...]handleSubmit = (e) => {
this.setState({ open: false });
var itemDetails = {
name: this.state.itemName,
price: this.state.itemPrice,
description: this.state.itemDescription,
}
console.log("Item Details : " + JSON.stringify(itemDetails))
API.graphql(graphqlOperation(mutations.createItem, {input: itemDetails}));
}
[...]
}
export default AddItem;

Once completing the above, go ahead and try adding an Item — you should see the following logged in the console, confirming a successful request.

And you should see something like this in your DynamoDB Table…

Woo, that’s awesome! But it’d be better if we could actually Read these items.

Read

Let’s quickly rectify this by displaying all of our saved items in our main view, by creating a new listItems component.

#bash$ touch src/components/listItems.js--------------------------------------------------------------------#src/components/listItems.jsimport React, {Component} from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import Grid from '@material-ui/core/Grid';
import { API, graphqlOperation } from "aws-amplify";
import * as queries from '../graphql/queries';
const styles = {
card: {
minWidth: 275,
},
bullet: {
display: 'inline-block',
margin: '0 2px',
transform: 'scale(0.8)',
},
title: {
fontSize: 14,
},
pos: {
marginBottom: 12,
},
root: {
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'inherit',
padding: '10px'
},
};
class ListItems extends Component {state = {
items: []
}
componentDidMount = () => {
this.getItems()
}
getItems = () => {
API.graphql(graphqlOperation(queries.listItems))
.then(data => this.setState({items: data.data.listItems.items}))
};
render(){
const { classes } = this.props;
const { items } = this.state;
const bull = <span className={classes.bullet}>•</span>;
console.log(items)
return (
<div className={classes.root}>
<Grid container className={classes.root} spacing={16}>
{items.map(item => (
<Grid key={item.id} item>
<Card className={classes.card}>
<CardContent>
<Typography className={classes.title} color="textSecondary" gutterBottom>
{item.name}
</Typography>
<Typography component="p">
£{item.price}
</Typography>
<br />
<Typography component="p">
{item.description}
</Typography>
</CardContent>
<CardActions>
<Button size="small">Edit Item</Button>
</CardActions>
</Card>
</Grid>
))}
</Grid>
</div>
);
}
}
ListItems.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(ListItems);--------------------------------------------------------------------
#src/screens/home.js
[...]
import ListItems from '../components/listItems'
class Home extends Component {
render() {
return (
<div>
<AppNavBar />
<ListItems />
</div>
);
}
}

I’ve added a few extra items but your main home view should look something like this:

Home Page

Breaking down our new listItem component briefly, you’ll notice we use another of our graphql queries, listItems, saved in the src/graphql/queries.js file. This retrieves all of the items in our database, which is fine for now but not really scalable long term. The query does allow for a limit argument, which can be specified when making the request. This will limit the amount of records fetched, however for now it’s not really required.

Edit

So we can create items and read them on our home view. Next let’s implement the ability to edit items. This is pretty quick and straightforward as we’ll re-use a lot of the code from our addItem component. We’ll also need to pass our new component our currentItem so we know what item is being edited. This will be done using props, passed from our listItems component.

First of all, let’s create a new module editItem.js. We’ll then import this in our listItems component, render it and pass it our currentItem.

#bash $ touch src/components/editItem.js--------------------------------------------------------------------#src/components/editItem.jsimport React, { Component } from 'react';
import TextField from '@material-ui/core/TextField';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import EditIcon from '@material-ui/icons/Edit';
import Button from '@material-ui/core/Button';
import { API, graphqlOperation } from "aws-amplify";
import * as mutations from '../graphql/mutations';
class EditItem extends Component {state = {
open: false,
itemName: '',
itemPrice: '',
itemDescription: ''
};
handleClickOpen = () => {
console.log("Current Item: " + this.props.currentItem.name)
this.setState({ open: true });
};
handleClose = () => {
this.setState({ open: false });
};
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
handleSubmit = (e) => {
this.setState({ open: false });
var itemDetails = {
id: this.props.currentItem.id,
name: this.state.itemName || this.props.currentItem.name,
price: this.state.itemPrice || this.props.currentItem.price,
description: this.state.itemDescription || this.props.currentItem.description
}
API.graphql(graphqlOperation(mutations.updateItem, {input: itemDetails}));
}
render() {
return (
<div style={{display: 'flex', flexWrap: 'wrap'}}>
<Button size='small' color="inherit" aria-label="Edit" onClick={this.handleClickOpen}>
<EditIcon />
</Button>
<Dialog
open={this.state.open}
onClose={this.handleClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">Edit Item: {this.props.currentItem.name}</DialogTitle>
<DialogContent>
<TextField
style={{marginRight: 10}}
id="itemName"
placeholder={this.props.currentItem.name}
label="Name"
type="string"
onChange={this.handleChange('itemName')}
/>
<TextField
style={{marginRight: 10}}
id="itemPrice"
placeholder={"£" + this.props.currentItem.price}
label="Price"
type="number"
onChange={this.handleChange('itemPrice')}
/>
<TextField
style={{marginTop: 10}}
multiline
id="itemDescription"
placeholder={this.props.currentItem.description}
label="Description"
type="string"
rows="4"
fullWidth
onChange={this.handleChange('itemDescription')}
/>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClose} color="primary">
Cancel
</Button>
<Button onClick={this.handleSubmit} color="primary">
Submit
</Button>
</DialogActions>
</Dialog>
</div>
);
}
}
export default EditItem;--------------------------------------------------------------------#src/components/listItems.js[...]import EditItem from './editItem'[...]<Grid container className={classes.root} spacing={16}>
{items.map(item => (
<Grid key={item.id} item>
<Card className={classes.card}>
<CardContent>
<Typography className={classes.title} color="textSecondary" gutterBottom>
{item.name}
</Typography>
<Typography component="p">
£{item.price}
</Typography>
<br />
<Typography component="p">
{item.description}
</Typography>
</CardContent>
<CardActions>
<EditItem currentItem={item}/>
</CardActions>
</Card>
</Grid>
))}

As you can see, the above looks pretty similar to our addItem component, however there are a few key differences. First of all, the mutation we call is different and the input we pass is slightly different in that we provide the ID of the item. This ensures we’re editing the correct item in our database. Also, in our itemDetails variable, we write a simple statement for each attribute: this.state.itemName || this.props.currentItem.name. This essentially results to if there’s a value to be submitted from the form, use that, else use the original value passed from our currentItem prop. As well as this, the form is slightly different. We use each attribute of the currentItem prop as a placeholder, so the user is able to see what’s being over written in each field.

If everything’s hooked up correctly you should see something similar to the below, after hitting the edit icon on item card. Go ahead and try updating one of your items, the changes should be reflected after reloading your app.

Delete

The last of our CRUD functions is delete. Similar to above, there’s a lot of re-used code and step’s are almost identical to above. We start by creating our new component and then also pass it currentItem as a prop, in our listItems component.

#bashtouch src/components/deleteItem.js--------------------------------------------------------------------#src/components/deleteItem.jsimport React, { Component } from 'react';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogTitle from '@material-ui/core/DialogTitle';
import DeleteIcon from '@material-ui/icons/Delete';
import Button from '@material-ui/core/Button';
import { API, graphqlOperation } from "aws-amplify";
import * as mutations from '../graphql/mutations';
class DeleteItem extends Component {state = {
open: false
};
handleClickOpen = () => {
console.log("Current Item: " + this.props.currentItem.name)
this.setState({ open: true });
};
handleClose = () => {
this.setState({ open: false });
};
handleDelete = () => {
this.setState({ open: false });
var itemDetails = {
id: this.props.currentItem.id,
}
API.graphql(graphqlOperation(mutations.deleteItem, { input: itemDetails }))
// window.location.reload()
};
render() {
return (
<div style={{display: 'flex', flexWrap: 'wrap'}}>
<Button style={{marginLeft: "125px"}}size='small' color="inherit" aria-label="Add" onClick={this.handleClickOpen}>
<DeleteIcon />
</Button>
<Dialog
open={this.state.open}
onClose={this.handleClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">Are you sure you want to delete item: {this.props.currentItem.name}?</DialogTitle>
<DialogActions>
<Button onClick={this.handleClose} color="primary">
Cancel
</Button>
<Button onClick={this.handleDelete} color="primary">
Delete
</Button>
</DialogActions>
</Dialog>
</div>
);
}
}
export default DeleteItem;--------------------------------------------------------------------
#src/components/listItem.js
import DeleteItem from './deleteItem'[...] {items.map(item => (
<Grid key={item.id} item>
<Card className={classes.card}>
<CardContent>
<Typography className={classes.title} color="textSecondary" gutterBottom>
{item.name}
</Typography>
<Typography component="p">
£{item.price}
</Typography>
<br />
<Typography component="p">
{item.description}
</Typography>
</CardContent>
<CardActions>
<EditItem currentItem={item}/>
<DeleteItem currentItem={item}/>
</CardActions>
</Card>
</Grid>
))}
[...]

As you can see, everything looks pretty similar here, however, one of the key differences is our handleDelete function. Here we call our deleteItem mutation and pass the item’s ID, this ensures the correct record is deleted from the database.

Once refreshed your app should reflect something similar to above. When clicking the delete button, and then refreshing, you’ll notice the item no longer appears and has been deleted from your database.

Congrats, you’ve now implemented basic CRUD functions within your GraphQL App! This gives you a great basic foundation to build upon and allows you to begin playing around with the the app.

In next part of the series, we’ll look at enhancing our app further through refining parts of the UI, adding user authentication and deploying our application via Amplify’s console.

All source code is available here.

As always, thanks for reading, hit 👏 if you like what you read and be sure to follow to keep up to date with future posts.

--

--